package com.googlecode.beanfiles.readers;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.beanfiles.DefaultTargetFactoryImpl;
import com.googlecode.beanfiles.DefaultTranslatorMapFactoryImpl;
import com.googlecode.beanfiles.TargetFactory;
import com.googlecode.beanfiles.TranslationArgumentException;
import com.googlecode.beanfiles.TranslationException;
import com.googlecode.beanfiles.TranslationNotice;
import com.googlecode.beanfiles.TranslatorsMapFactory;
import com.googlecode.beanfiles.translators.PropertyTranslator;


/**
 * Provides an iterator to read target objects of type T.
 * 
 * @param <T>
 */
public class ReaderIteratorBase<T> implements ReaderIterator<T>, Iterable<T> {

    protected static Log log() {
        return LogFactory.getLog(ReaderIteratorBase.class);
    }

    private Map<String, ? extends PropertyTranslator> translators;
    protected Iterator<Collection<Object>> iterator;
    private TargetFactory<T> targetFactory;
    private int lineNumber = 0;
    private TranslatorsMapFactory mapFactory;

    public ReaderIteratorBase(Class<T> target, Iterator<Collection<Object>> input, Map<String, ? extends PropertyTranslator> translators) {
        this(target, input);
        this.mapFactory = new DefaultTranslatorMapFactoryImpl();
        this.mapFactory.setCustomTranslators(translators);
    }

    /**
     * Constructs a reader iterator with a default target factory implementation, and an ordered map of translators based on the headingMap.
     * The rules for creating translators are 1) if a property translator can be found in the provided translators map, it is used or 2) a
     * default implementation is provided.
     * 
     * @param target
     * @param translators
     * @param headingMap
     */
    public ReaderIteratorBase(Class<T> target, Map<String, ? extends PropertyTranslator> translators, Map<String, String> headingMap,
            Iterator<Collection<Object>> input) {
        this.targetFactory = new DefaultTargetFactoryImpl<T>(target);
        this.iterator = input;
        this.mapFactory = new DefaultTranslatorMapFactoryImpl(translators, headingMap);
        this.translators = mapFactory.buildMap(iterator.next(), targetFactory);
        lineNumber++;
    }

    public ReaderIteratorBase(Class<T> target, Iterator<Collection<Object>> input) {
        this.targetFactory = new DefaultTargetFactoryImpl<T>(target);
        this.iterator = input;
        this.mapFactory = new DefaultTranslatorMapFactoryImpl();
        translators = mapFactory.buildMap(iterator.next(), targetFactory);
        lineNumber++;
    }

    public ReaderIteratorBase(Class<T> target, Map<String, String> headingMap, Iterator<Collection<Object>> input) {
        this.targetFactory = new DefaultTargetFactoryImpl<T>(target);
        this.iterator = input;
        this.mapFactory = new DefaultTranslatorMapFactoryImpl(headingMap);
        translators = mapFactory.buildMap(iterator.next(), targetFactory);
        lineNumber++;
    }

    public Iterator<T> iterator() {
        return this;
    }

    public boolean hasNext() {
        return iterator.hasNext();
    }

    public T next() {
        lineNumber++;
        Collection<Object> i = iterator.next();
        T targetInstance = targetFactory.createTarget();
        Object translatedValue = null;
        try {
            for (PropertyDescriptor descriptor : Introspector.getBeanInfo(targetInstance.getClass()).getPropertyDescriptors()) {
                PropertyTranslator translator = translators.get(descriptor.getName());
                if (translator != null) {
                    try {
                        translatedValue = translator.read(i, descriptor.getPropertyType());
                        descriptor.getWriteMethod().invoke(targetInstance, translatedValue);

                        // set the line number of all translation notices and add to collection of notices
                        addTranslationNotices(translator, targetInstance, descriptor.getName());
                        //clear for next pass
                        translator.getTranslationNotices().clear();
                    } catch (TranslationArgumentException e) {
                        // hook for converters and translators to signal invalid values
                        addTranslationNotice(translator, targetInstance, new TranslationNotice(e.getValue(), descriptor.getName()));
                    }
                }
            }
        } catch (IllegalArgumentException e) {
            // interested in IllegalArgumentExceptions because they are type conversion errors.
            log().error(e);
            throw new TranslationException(
                "Translator threw illegal argument exception. Catch and rethrow as TranslationArgumentException for users notices.", e);
        } catch (InvocationTargetException e) {
            log().error(e);
            throw new TranslationException(e);
        } catch (IntrospectionException e) {
            log().error(e);
            throw new TranslationException(e);
        } catch (IllegalAccessException e) {
            log().error(e);
            throw new TranslationException(e);
        }
        return targetInstance;
    }

    public void remove() {}

    private Map<T, List<TranslationNotice>> noticesMap = new HashMap<T, List<TranslationNotice>>();

    private void addTranslationNotice(PropertyTranslator translator, T target, TranslationNotice notice) {
        notice.setRow(lineNumber);
        List<TranslationNotice> notices = noticesMap.get(target);
        if (notices == null) {
            notices = new ArrayList<TranslationNotice>();
            noticesMap.put(target, notices);
        }
        notices.add(notice);
    }

    private void addTranslationNotices(PropertyTranslator translator, T target, String propertyName) {
        for (TranslationNotice notice : translator.getTranslationNotices()) {
            notice.setRow(lineNumber);
            if (notice.getPropertyName() == null) {
                notice.setPropertyName(propertyName);
            }
            List<TranslationNotice> notices = noticesMap.get(target);
            if (notices == null) {
                notices = new ArrayList<TranslationNotice>();
                noticesMap.put(target, notices);
            }
            if (!notices.contains(notice)) {
                notices.add(notice);
            }
        }
    }

    public List<TranslationNotice> getTranslationNotices(T target, String propertyName) {
        List<TranslationNotice> notices = new ArrayList<TranslationNotice>();
        if (getTranslationNotices().get(target) == null) {
            return null;
        }
        for (TranslationNotice notice : getTranslationNotices().get(target)) {
            if (notice.getPropertyName().equals(propertyName)) {
                notices.add(notice);
            }
        }
        return notices;
    }

    public Map<T, List<TranslationNotice>> getTranslationNotices() {
        return noticesMap;
    }

    public List<Integer> getColumns(String propertyName) {
        PropertyTranslator mappedTranslator = translators.get(propertyName);
        if (mappedTranslator != null) {
            return mappedTranslator.getIndexes(propertyName);
        } else {
            for (PropertyTranslator translator : translators.values()) {
                if (translator.resolvesProperty(propertyName)) {
                    return translator.getIndexes(propertyName);
                }
            }
        }
        throw new TranslationException("No Translator found for property " + propertyName);
    }

}
